home *** CD-ROM | disk | FTP | other *** search
- #!/usr/bin/perl
-
- =head1 NAME
-
- gpgApplet - GNOME applet for OpenGPG text encryption
-
- =head1 DESCRIPTION
-
- gpgApplet allows encrypting and decrypting the content of the
- clipboard with a symmetric cipher using a passphrase. This is
- a graphical frontend on top of GnuPG.
-
- Asymmetric decryption and clipboard verification are also supported.
-
- =head1 PREREQUISITES
-
- gpgApplet does not handle passphrase input. Since it also does not
- offer terminal interaction unless explicitly run from there, it relies
- in practice on some kind of GnuPG agent such as pinentry, Seahorse 2.x
- or GNOME keyring 3.x to manage passphrase input.
-
- =cut
-
- use strict;
- use warnings FATAL => 'all';
- use 5.10.0;
-
- # VERSION
-
- use Glib qw{TRUE FALSE};
- use Gtk2 qw{-init};
- use Gtk2::Gdk::Keysyms;
- use Gtk2::SimpleList;
-
- use Encode qw{decode encode find_encoding};
- use English;
- use gpgApplet::GnuPG::Interface;
- use IO::Handle;
- use I18N::Langinfo qw{langinfo CODESET};
- use List::MoreUtils qw{none};
- use Switch;
- use DateTime;
-
- use Locale::gettext;
- use POSIX;
- setlocale(LC_MESSAGES, "");
- textdomain("gpgApplet");
-
-
- =head1 GLOBALS
-
- =cut
-
- use constant C_SELECT => 0;
- use constant C_NAME => 1;
- use constant C_KEYID => 2;
- use constant C_STATUS => 3;
- use constant C_FINGERPRINT => 4;
- use constant C_USERIDS => 5;
- use constant C_TRUSTED => 6;
- use constant VISIBLE_COLS => (C_NAME, C_KEYID, C_STATUS);
- use constant HIDDEN_COLS => (C_FINGERPRINT, C_USERIDS, C_TRUSTED);
-
- use constant COMBO_NAME => 0;
- use constant COMBO_KEYID => 1;
- use constant COMBO_FINGERPRINT => 2;
- use constant COMBO_ROLE => 3;
-
- my $gnupg = gpgApplet::GnuPG::Interface->new();
- my $codeset = langinfo(CODESET());
- my $encoding = find_encoding($codeset);
- my $main_window = Gtk2::Window->new();
- my $icon_factory = Gtk2::IconFactory->new();
- # Set always_trust since GnuPG otherwise will fail if the key's
- # trust hasn't been set.
- my %gnupg_options = (armor => 1, always_trust => 0, meta_interactive => 0);
-
- my $pgp_encrypted_msg = {
- type => 'message',
- header => '-----BEGIN PGP MESSAGE-----',
- footer => '-----END PGP MESSAGE-----'
- };
- my $pgp_signed_msg = {
- type => 'signed',
- header => '-----BEGIN PGP SIGNED MESSAGE-----',
- middle => '-----BEGIN PGP SIGNATURE-----',
- footer => '-----END PGP SIGNATURE-----'
- };
- my @pgp_headers = ($pgp_encrypted_msg, $pgp_signed_msg);
-
- =head1 MAIN
-
- =cut
-
- my $statusicon = build_statusicon();
- $statusicon->set_visible(TRUE);
- init_freshest_clipboard();
- init_icons_stock($icon_factory);
- detect_received(freshest_clipboard());
- Gtk2->main;
-
-
- =head1 FUNCTIONS
-
- =cut
-
- sub all_clipboards {
- map {
- Gtk2::Clipboard->get($_)
- } (
- Gtk2::Gdk->SELECTION_CLIPBOARD,
- Gtk2::Gdk->SELECTION_PRIMARY
- );
- }
-
- {
- my $freshest_clipboard;
-
- sub init_freshest_clipboard {
- $freshest_clipboard = Gtk2::Clipboard->get(Gtk2::Gdk->SELECTION_CLIPBOARD);
- }
-
- sub freshest_clipboard {
- return $freshest_clipboard;
- }
-
- sub set_freshest_clipboard {
- $freshest_clipboard = shift;
- }
- }
-
- sub build_statusicon {
- my $icon = Gtk2::StatusIcon->new;
- $icon->set_visible(FALSE);
- $icon->set_from_icon_name('seahorse');
- $icon->set_tooltip($encoding->decode(gettext('OpenPGP encryption applet')));
-
- my $menu = Gtk2::Menu->new;
- my $mexit = Gtk2::MenuItem->new($encoding->decode(gettext('Exit')));
- $mexit->signal_connect('activate' => sub { Gtk2->main_quit; });
- my $mabout = Gtk2::MenuItem->new($encoding->decode(gettext('About')));
- $mabout->signal_connect('activate' => sub { Gtk2->show_about_dialog(
- $main_window,
- 'program-name' => 'gpgApplet',
- 'license' => q{This program is free software; you can redistribute it and/or modify it under the terms of either:
-
- a) the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version, or
-
- b) the "Artistic License" which comes with Perl.
- },
- 'wrap-license' => 1,
- 'website' => 'https://tails.boum.org/',
- )});
- $menu->append($mexit);
- $menu->append(Gtk2::SeparatorMenuItem->new);
- $menu->append($mabout);
-
- $icon->signal_connect('popup-menu', sub {
- my $ticon = shift;
- my $event = shift;
- my $time = shift;
- $menu->show_all;
- $menu->popup(undef, undef, undef, undef, $event, $time);
- });
-
- $icon->signal_connect('button-press-event' => sub {
- my $ticon = shift;
- my $event = shift;
- return unless $event->button == 1;
- my $action_menu = build_action_menu();
- $action_menu->show_all;
- $action_menu->popup(undef, undef, undef, undef, $event->button, $event->time);
- });
-
- foreach (all_clipboards()) {
- $_->signal_connect("owner-change" => sub {
- my $clipboard = shift;
- my $event = shift;
- handle_clipboard_owner_change($clipboard);
- });
- }
-
- return $icon;
- }
-
- sub build_action_menu {
- my $action_menu = Gtk2::Menu->new;
-
- my $text_type = detect_text_type(get_validated_clipboard_text());
-
- if ($text_type eq 'text' or $text_type eq 'none') {
- my $msymencrypt = Gtk2::MenuItem->new_with_mnemonic($encoding->decode(gettext('Encrypt Clipboard with _Passphrase')));
- $msymencrypt->signal_connect('activate' => sub { operate_on_clipboard(\&symmetric_encrypt, ['text']); });
- $action_menu->append($msymencrypt);
- my $msignencrypt = Gtk2::MenuItem->new_with_mnemonic($encoding->decode(gettext('Sign/Encrypt Clipboard with Public _Keys')));
- $msignencrypt->signal_connect('activate' => sub { operate_on_clipboard(\&public_crypto, ['text']); });
- $action_menu->append($msignencrypt);
- }
- if ($text_type eq 'message' or $text_type eq 'signed') {
- my $mdecryptver = Gtk2::MenuItem->new_with_mnemonic($encoding->decode(gettext('_Decrypt/Verify Clipboard')));
- $mdecryptver->signal_connect('activate' => sub { operate_on_clipboard(\&decrypt_verify, ['message', 'signed']); });
- $action_menu->append($mdecryptver);
- }
- my $mmanage = Gtk2::MenuItem->new_with_mnemonic($encoding->decode(gettext('_Manage Keys')));
- $mmanage->signal_connect('activate' => sub { manage_keys(); });
- $action_menu->append($mmanage);
-
- return $action_menu;
- }
-
- sub manage_keys {
- system("seahorse &");
- }
-
- sub all_text_types {
- map { $_->{type} } @pgp_headers;
- }
-
- sub detect_text_type {
- my $text = shift;
-
- unless (defined $text && length($text)) {
- return 'none';
- }
-
- foreach (@pgp_headers) {
- my $header = $_->{header};
- my $footer = $_->{footer};
- return $_->{type} if $text =~ m{$header.*$footer}ms;
- }
-
- return 'text';
- }
-
- sub text_is_of_type {
- my $text = shift;
- my @valid_types = @_;
-
- my $text_type = detect_text_type($text);
- if (none { $_ eq $text_type } @valid_types) {
- return (
- 0,
- $encoding->decode(
- gettext("The clipboard does not contain valid input data."))
- );
- }
- return (1);
- }
-
- sub get_validated_clipboard_text {
- my $args = shift;
- my @valid_types;
-
- if (exists $args->{valid_types} && defined $args->{valid_types}) {
- @valid_types = @{ $args->{valid_types} };
- }
- else {
- @valid_types = all_text_types();
- }
-
- my $clipboard = freshest_clipboard();
- # Note: according to the GTK documentation, the wait_for_text method
- # is supposed to always returns UTF-8. But it seems like the Perl
- # bindings decode it and we get a string of chars instead of bytes.
- my $content = $clipboard->wait_for_text;
- my ($is_valid, $reason) = text_is_of_type($content, @valid_types);
- return ($content) if $is_valid;
- return (0, $reason);
- }
-
- sub set_clipboards_text {
- my $text = shift;
- my $encoded_text = $encoding->encode($text);
-
- # Note: according to the GTK documentation, the set_text method
- # is supposed to need input encoded in UTF-8. But it seems like the Perl
- # bindings encode it, and we need to pass a string of chars instead of bytes.
- foreach (all_clipboards()) {
- $_->set_text($encoded_text);
- }
- # GTK fails setting the primary selection above, so let's use xclip :/
- open(my $xclip, '| xclip') or die "Error opening pipe to xclip";
- print $xclip $encoded_text or die "Error copying data to X clipboard";
- close $xclip or die "Error closing pipe to xclip";
- }
-
- sub get_status {
- my $code = shift;
- my $status;
- my $trusted;
- # Below taken from doc/DETAILS in GnuPG's sources
- switch ($code) {
- case "o" { $trusted = FALSE;
- $status = $encoding->decode(gettext("Unknown Trust")); }
- case "-" { $trusted = FALSE;
- $status = $encoding->decode(gettext("Unknown Trust")); }
- case "q" { $trusted = FALSE;
- $status = $encoding->decode(gettext("Unknown Trust")); }
- case "m" { $trusted = FALSE;
- $status = $encoding->decode(gettext("Marginal Trust")); }
- case "f" { $trusted = TRUE;
- $status = $encoding->decode(gettext("Full Trust")); }
- case "u" { $trusted = TRUE;
- $status = $encoding->decode(gettext("Ultimate Trust")); }
- else { return; }
- }
- return ($status, $trusted);
- }
-
- sub get_private_key_status {
- my $key = shift;
-
- my $fingerprint = $encoding->decode($key->fingerprint->as_hex_string());
- my $pubkey = ($gnupg->get_public_keys_light($fingerprint))[0]; # ignore collisions
-
- # a valid key may lack signing capabilities
- return unless $pubkey->usage_flags =~ m/S/;
-
- my $validity = $pubkey->user_ids->[0]->validity;
- return get_status($validity);
- }
-
- sub get_public_key_status {
- my $key = shift;
-
- # a valid key may lack encryption capabilities
- return unless $key->usage_flags =~ m/E/;
-
- my $validity = $key->user_ids->[0]->validity;
- return get_status($validity);
- }
-
- sub create_key_row {
- my $key = shift;
-
- my ($status, $trusted) = (ref($key) eq "GnuPG::SecretKey") ?
- get_private_key_status($key)
- : get_public_key_status($key);
- # no status implies expired, revoked, etc. keys, which we don't want to list
- return if !defined $status;
-
- my $name = $encoding->decode($key->user_ids->[0]->as_string);
- my $userids = join("\n", map { my $a = $_->as_string; my $b = $encoding->decode("$a"); my $c ="x" . "$b" }
- $key->user_ids);
- my $keyid = $encoding->decode($key->short_hex_id);
-
- my $fingerprint = $encoding->decode($key->fingerprint->as_hex_string());
- # Gtk2::SimpleList encodes these strings itself.
- return [FALSE, $name, $keyid, $status, $fingerprint, $userids, $trusted];
- }
-
- sub make_pub_key_list {
- my $pub_keys_ref = shift;
-
- my $list = Gtk2::SimpleList->new (
- "" => 'bool', # C_SELECT
- $encoding->decode(gettext("Name")) => 'text', # C_NAME
- $encoding->decode(gettext("Key ID")) => 'text', # C_KEYID
- $encoding->decode(gettext("Status")) => 'text', # C_STATUS
- "" => 'text', # C_FINGERPRINT
- "" => 'text', # C_USERIDS
- "" => 'bool' # C_TRUSTED
- );
- foreach my $i (VISIBLE_COLS) {
- my $col = $list->get_column($i);
- $col->set_max_width(400);
- $col->set_resizable(TRUE);
- $col->set_sort_column_id($i);
- }
- foreach my $i (HIDDEN_COLS) {
- $list->get_column($i)->set_visible(FALSE);
- }
- $list->set_search_column(C_NAME);
- $list->get_selection->set_mode('single');
- $list->get_selection->unselect_all;
- # Initially sort by name (couldn't find a cleaner way)
- $list->get_column(C_NAME)->signal_emit('clicked');
-
- # If we used Gtk2::TreeView instead of Gtk2::SimpleList we could
- # show all user ids directly in the list, but we make it simple
- # for us and instead show them in the tooltip.
- $list->set_has_tooltip(TRUE);
- $list->signal_connect('query-tooltip' => sub {
- my ($wx, $wy, $tooltip) = ($_[1], $_[2], $_[4]);
- my ($x, $y) = $list->convert_widget_to_bin_window_coords($wx, $wy);
- my $path = $list->get_path_at_pos($x, $y);
- return FALSE unless defined $path;
- my $row = ($path->get_indices)[0];
- my $fingerprint =
- join(" ", (${$list->{data}}[$row][C_FINGERPRINT] =~ m/..../g));
- my $fingerprint_label = $encoding->decode(gettext("Fingerprint:"));
- my $uids = "${$list->{data}}[$row][C_USERIDS]";
- my $uids_label = $encoding->decode(
- ngettext("User ID:", "User IDs:", ($uids =~ tr/\n//) + 1));
- my $text = sprintf("%s\n%s\n%s\n%s", $uids_label, $uids,
- $fingerprint_label, $fingerprint);
- $tooltip->set_text("$text");
- return TRUE;
- });
-
- $list->signal_connect('row-activated' => sub {
- # Since we use 'single' selection mode, there can only be one
- my $index = ($list->get_selected_indices)[0];
- my $old_val = $list->{data}->[$index]->[C_SELECT];
- $list->{data}->[$index]->[C_SELECT] = !$old_val;
- });
-
- push @{$list->{data}},
- grep { $_ } map { create_key_row($_) } @{$pub_keys_ref};
-
- $list->select(0);
-
- return $list;
- }
-
- sub make_priv_key_combo {
- my $priv_keys_ref = shift;
-
- my $list_store = Gtk2::ListStore->new(
- qw/Glib::String Glib::String Glib::String Glib::String/);
- my $iter = $list_store->append;
- $list_store->set ($iter,
- COMBO_NAME, $encoding->decode(gettext(
- "None (Don't sign)")),
- COMBO_KEYID, "",
- COMBO_FINGERPRINT, "",
- COMBO_ROLE, "none");
- $iter = $list_store->append;
- $list_store->set ($iter,
- COMBO_NAME, "",
- COMBO_KEYID, "",
- COMBO_FINGERPRINT, "",
- COMBO_ROLE, "separator");
- foreach my $key (@{$priv_keys_ref}) {
- my $row = create_key_row($key);
- next unless $row; # skip keys without signing capability
- $iter = $list_store->append;
- $list_store->set ($iter,
- COMBO_NAME, "$row->[C_NAME]",
- COMBO_KEYID, "($row->[C_KEYID])",
- COMBO_FINGERPRINT, "$row->[C_FINGERPRINT]",
- COMBO_ROLE, "");
- }
-
- my $sorted_list = Gtk2::TreeModelSort->new_with_model($list_store);
- $sorted_list->set_default_sort_func(sub {
- my ($model, $iter1, $iter2, $data) = @_;
- my $name1 = $model->get($iter1, COMBO_NAME);
- my $name2 = $model->get($iter2, COMBO_NAME);
- my $role1 = $model->get($iter1, COMBO_ROLE);
- my $role2 = $model->get($iter2, COMBO_ROLE);
-
- if ($role1 eq "none") {
- return -1;
- } elsif ($role2 eq "none") {
- return 1;
- } elsif ($role1 eq "separator") {
- return -1;
- } elsif ($role2 eq "separator") {
- return 1;
- } else {
- return (lc $name1 cmp lc $name2);
- }
- });
-
- my $combo = Gtk2::ComboBox->new_with_model($sorted_list);
- my $renderer = Gtk2::CellRendererText->new();
- $combo->pack_start($renderer, FALSE);
- $combo->add_attribute($renderer, 'text', COMBO_NAME);
- $renderer = Gtk2::CellRendererText->new();
- $combo->pack_start($renderer, FALSE);
- $combo->add_attribute($renderer, 'text', COMBO_KEYID);
- $combo->set_row_separator_func( sub {
- my ($model, $iter, $data) = @_;
- return TRUE if ($model->get($iter, COMBO_ROLE) eq "separator");
- });
- $combo->set_active(0);
-
- return $combo;
- }
-
- sub choose_keys {
- my $priv_keys_ref = shift;
- my $pub_keys_ref = shift;
-
- my $pub_key_label = Gtk2::Label->new(
- $encoding->decode(gettext("Select recipients:")));
-
- my $pub_key_list = make_pub_key_list($pub_keys_ref);
- my $pub_key_list_scroll = Gtk2::ScrolledWindow->new;
- $pub_key_list_scroll->set_policy('automatic', 'always');
- $pub_key_list_scroll->add($pub_key_list);
-
- my $hide_recipients_checkbox = Gtk2::CheckButton->new(
- $encoding->decode(gettext("Hide recipients")));
- $hide_recipients_checkbox->set_has_tooltip(TRUE);
- $hide_recipients_checkbox->set_tooltip_text(
- $encoding->decode(gettext("Hide the user IDs of all recipients of " .
- "an encrypted message. Otherwise anyone " .
- "that sees the encrypted message can see " .
- "who the recipients are.")));
-
- my $priv_key_label = Gtk2::Label->new(
- $encoding->decode(gettext("Sign message as:")));
-
- my $priv_key_combo = make_priv_key_combo($priv_keys_ref);
-
- my $dialog = Gtk2::Dialog->new($encoding->decode(gettext("Choose keys")),
- $main_window, 'destroy-with-parent',
- 'gtk-cancel' => 'cancel', 'gtk-ok' => 'ok' );
- $dialog->set_default_size(650,500);
- $dialog->set_default_response('ok');
- my $vbox = $dialog->get_content_area;
- $vbox->pack_start($pub_key_label, FALSE, FALSE, 5);
- $vbox->pack_start($pub_key_list_scroll, TRUE, TRUE, 0);
- my $hbox = Gtk2::HBox->new;
- $hbox->pack_start($priv_key_label, FALSE, FALSE, 0);
- $hbox->pack_start($priv_key_combo, TRUE, TRUE, 0);
- $vbox->pack_start($hbox, FALSE, FALSE, 5);
- $vbox->pack_start($hide_recipients_checkbox, FALSE, FALSE, 0);
-
- $pub_key_list->grab_focus;
- $dialog->show_all;
-
- $dialog->signal_connect('key-press-event' => sub {
- my $event = $_[1];
- return unless $event->keyval == $Gtk2::Gdk::Keysyms{Return};
- $dialog->response('ok');
- return 1;
- });
-
- while ($dialog->run eq 'ok') {
- my @recipients;
- my $signer;
- my $always_trust = 0;
-
- # Get signing key, if any
- my $priv_key_combo_model = $priv_key_combo->get_model;
- my $priv_key_iter = $priv_key_combo->get_active_iter;
- $signer = $priv_key_combo_model->get($priv_key_iter, COMBO_FINGERPRINT);
-
- # Get public keys, if any
- my @list_selection = grep { $_->[C_SELECT] } @{$pub_key_list->{data}};
- if (@list_selection) {
- my @unauth = grep { ! $_->[C_TRUSTED] } @list_selection;
- if (@unauth) {
- my $title = $encoding->decode(gettext(
- "Do you trust these keys?"
- ));
- my $warning = $encoding->decode(ngettext(
- "The following selected key is not fully trusted:",
- "The following selected keys are not fully trusted:",
- scalar @unauth
- ));
- my $msg = sprintf("%s\n", $warning);
- foreach my $key (@unauth) {
- # Each key will be listed RTL *or* LTR depending on the
- # direction of the first character of the name. This
- # unfortunately causes mixing of LTR and RTL in the
- # same list. A potential FIXME would be to display this
- # with a custom windows using SimpleList for the keys.
- # Also note that everything in $key (which originates
- # from $pub_key_list) already has been decoded, so we
- # don't have to do it again here.
- my $key_name = "$key->[C_NAME] ($key->[C_KEYID])";
- $msg = sprintf("%s%s\n", $msg, $key_name);
- }
- my $question = $encoding->decode(ngettext(
- "Do you trust this key enough to use it anyway?",
- "Do you trust these keys enough to use them anyway?",
- scalar @unauth
- ));
- $msg = sprintf("%s%s", $msg, $question);
- next unless display_question($dialog, $title, $msg);
- $always_trust = 1;
- }
- @recipients = map { $_->[C_FINGERPRINT] } @list_selection;
- }
-
- if (!@recipients && !$signer) {
- display_error($dialog,
- $encoding->decode(gettext("No keys selected")),
- $encoding->decode(gettext(
- "You must select a private key to sign the " .
- "message, or some public keys to encrypt the " .
- "message, or both."
- )));
- next;
- }
-
- $dialog->destroy;
- return {
- always_trust => $always_trust,
- hide_recipients => $hide_recipients_checkbox->get_active,
- signer => $signer,
- recipients => \@recipients,
- };
- }
- $dialog->destroy;
- return ();
- }
-
- sub public_crypto {
- my $args = shift;
- my $handles = $args->{handles};
-
- my @priv_keys = $gnupg->get_secret_keys_light();
- my @pub_keys = $gnupg->get_public_keys_light();
-
- if (@priv_keys == 0 && @pub_keys == 0) {
- display_error($main_window,
- $encoding->decode(gettext("No keys available")),
- $encoding->decode(gettext(
- "You need a private key to sign messages or a " .
- "public key to encrypt messages."
- )));
- return 0;
- }
-
- my $chosen = choose_keys(\@priv_keys, \@pub_keys);
- my $signer = $chosen->{signer};
- my $recipients_ref = $chosen->{recipients};
- my @recipients = @{$recipients_ref} if defined $recipients_ref;
- my $always_trust = $chosen->{always_trust};
- my $hide_recipients = $chosen->{hide_recipients};
-
- $gnupg->options->always_trust($always_trust);
- $gnupg->options->clear_extra_args;
- $gnupg->options->clear_meta_signing_key_id;
- $gnupg->options->clear_recipients();
-
- if ($signer) {
- $gnupg->options->meta_signing_key_id($signer);
- }
-
- if (@recipients) {
- if ($hide_recipients) {
- # Since gpg's --no-throw-keyids seems to be broken (it doesn't
- # work via the CLI either) we can't just push it to extra_args :/.
- foreach my $recipient (@recipients) {
- $gnupg->options->push_extra_args('--hidden-recipient',
- $recipient);
- }
- } else {
- $gnupg->options->push_recipients(@recipients);
- }
- }
-
- my $result = 0;
-
- if ($signer && !@recipients) {
- $result = $gnupg->clearsign(handles => $handles);
- } elsif (@recipients && !$signer) {
- $result = $gnupg->encrypt(handles => $handles);
- } elsif ($signer && @recipients) {
- $result = $gnupg->sign_and_encrypt(handles => $handles);
- }
-
- $gnupg->options->always_trust(0);
- $gnupg->options->clear_extra_args;
- $gnupg->options->clear_meta_signing_key_id;
- $gnupg->options->clear_recipients();
-
- return $result;
- }
-
- sub symmetric_encrypt {
- my $args = shift;
- my $handles = $args->{handles};
-
- return $gnupg->encrypt_symmetrically(handles => $handles);
- }
-
- sub decrypt_verify {
- my $args = shift;
- my $handles = $args->{handles};
- my $input = $args->{input};
-
- my $text_type = detect_text_type($input);
- return
- $text_type eq 'message'
- ? $gnupg->decrypt(handles => $handles)
- : $gnupg->verify(handles => $handles);
- }
-
- sub gpg_operate_on_text {
- my $operation = shift;
- my $text = shift;
-
- $gnupg->options->hash_init(%gnupg_options);
- my $in_h = IO::Handle->new();
- my $err_h = IO::Handle->new();
- my $out_h = IO::Handle->new();
- my $handles = GnuPG::Handles->new(
- stdin => $in_h,
- stderr => $err_h,
- stdout => $out_h
- );
-
- my $args = {
- handles => $handles,
- input => $text,
- in_h => $in_h,
- err_h => $err_h,
- out_h => $out_h,
- };
-
- my $pid = $operation->($args) or return;
-
- # We assume the sender/recipient uses the same charset as us :/
- # PGP/MIME was invented for a reason.
- print $in_h $encoding->encode($text);
- close $in_h;
-
- my ($err, $out) = read_err_out($err_h, $out_h);
- my @raw_stderr = @{$err};
- my @raw_stdout = @{$out};
-
- waitpid $pid, 0; # Clean up the finished GnuPG process.
-
- my $std_err = $encoding->decode(join('', @raw_stderr));
- my $std_out = $encoding->decode(join('', @raw_stdout));
-
- if ($CHILD_ERROR == 0) {
- if ($operation eq \&decrypt_verify) {
- my $msg;
- if ($text =~ m/$pgp_signed_msg->{header}/) {
- $msg = $text;
- $msg =~ s/^.*$pgp_signed_msg->{header}\nHash: [^\n]*\n\n//m;
- $msg =~ s/^$pgp_signed_msg->{middle}.*//ms;
- } else {
- $msg = $std_out;
- }
- display_output($msg, $std_err);
- } else {
- set_clipboards_text($std_out);
- }
- }
- else {
- display_error(
- $main_window,
- $encoding->decode(gettext("GnuPG error")),
- $std_out . "\n\n" . $std_err
- );
- return;
- }
-
- return;
- }
-
- sub operate_on_clipboard {
- my $operation = shift;
- my $valid_types = shift;
-
- my ($text, $clip_error) = get_validated_clipboard_text(
- { valid_types => $valid_types }
- );
-
- if (defined $clip_error) {
- display_error(
- $main_window,
- $clip_error, # already translated and decoded
- $encoding->decode(gettext("Therefore the operation cannot be " .
- "performed."))
- );
- return;
- }
-
- gpg_operate_on_text($operation, $text);
- }
-
- sub display_error {
- my $parent = shift;
- my $title = shift;
- my $msg = shift;
-
- my $dialog = Gtk2::MessageDialog->new(
- $parent, 'destroy-with-parent', 'error', 'ok',
- $title
- );
- $dialog->format_secondary_text($msg);
- $dialog->signal_connect(
- response => sub { my $self = shift; $self->destroy; }
- );
- $dialog->set_position('center');
- $dialog->run;
- $dialog->destroy;
-
- return 1;
- }
-
- sub display_question {
- my $parent = shift;
- my $title = shift;
- my $msg = shift;
-
- my $dialog = Gtk2::MessageDialog->new(
- $parent, 'destroy-with-parent', 'question', 'yes-no', $title);
- $dialog->format_secondary_text($msg);
- $dialog->set_position('center');
- my $answer = $dialog->run;
- $dialog->destroy;
- return $answer eq 'yes' ? TRUE : FALSE;
- }
-
- # FIXME: let window grow depending on output text size
- sub display_output {
- my $std_out = shift;
- my $std_err = shift;
-
- my $dialog = Gtk2::MessageDialog->new(
- $main_window, 'destroy-with-parent', 'info', 'ok',
- $encoding->decode(gettext("GnuPG results"))
- );
- my $my_width_request = 800;
- my $my_height_request = 600;
- # TRANSLATORS: GnuPG stdout (encrypted or decrypted message)
- $dialog->format_secondary_text(sprintf($encoding->decode(gettext(
- "Output of GnuPG:"
- ))));
-
- my $msg_area = $dialog->get_content_area;
-
- my $outbuf = Gtk2::TextBuffer->new();
- $outbuf->set_text($std_out);
- my $text_desc = Pango::FontDescription->new;
- $text_desc->set_family('Monospace');
- my $textview_out = Gtk2::TextView->new_with_buffer($outbuf);
- $textview_out->set_editable(FALSE);
- $textview_out->set_cursor_visible(FALSE);
- $textview_out->set_left_margin(10);
- $textview_out->set_right_margin(10);
- $textview_out->set_wrap_mode('word');
- $textview_out->modify_font($text_desc);
- my $scrolled_win_out = Gtk2::ScrolledWindow->new;
- $scrolled_win_out->set_policy('automatic', 'automatic');
- $scrolled_win_out->add($textview_out);
- $msg_area->pack_start($scrolled_win_out, TRUE, TRUE, 0);
-
- if (defined $std_err && length($std_err)) {
- my $std_err_title = Gtk2::Label->new(
- # TRANSLATORS: GnuPG stderr (other informational messages)
- $encoding->decode(gettext(
- "Other messages provided by GnuPG:"
- )));
- $std_err_title->set_alignment(0, 0);
- $std_err_title->set_padding(10, 0);
- $msg_area->pack_start($std_err_title, FALSE, FALSE, 0);
- my $std_err_buf = Gtk2::TextBuffer->new();
- $std_err_buf->set_text($std_err);
- my $textview_err = Gtk2::TextView->new_with_buffer($std_err_buf);
- $textview_err->set_editable(FALSE);
- $textview_err->set_cursor_visible(FALSE);
- $textview_err->set_left_margin(10);
- $textview_err->set_right_margin(10);
- $textview_err->set_wrap_mode('word');
- $textview_err->modify_font($text_desc);
- my $scrolled_win_err = Gtk2::ScrolledWindow->new;
- $scrolled_win_err->set_policy('automatic', 'automatic');
- $scrolled_win_err->add($textview_err);
- $scrolled_win_err->set_size_request(-1, $my_height_request/5);
- $msg_area->pack_start($scrolled_win_err, FALSE, FALSE, 0);
- }
-
- $dialog->signal_connect(
- response => sub { my $self = shift; $self->destroy; }
- );
- my $screen_width = $dialog->get_screen()->get_width();
- my $screen_height = $dialog->get_screen()->get_height();
- if ($screen_width > $my_width_request || $screen_height > $my_height_request) {
- $dialog->set_size_request($my_width_request, $my_height_request);
- } else {
- $dialog->maximize();
- }
- $dialog->set_resizable(TRUE);
- $dialog->set_position('center');
- $dialog->show_all;
-
- return 1;
- }
-
- # Read stdout and stderr at the same time, one line at a time, to
- # avoid dead-locking due to one of the buffers being full.
- sub read_err_out {
- my $err_h = shift;
- my $out_h = shift;
-
- my $err = [];
- my $out = [];
-
- while (1) {
- my $err_l = <$err_h>;
- my $out_l = <$out_h>;
- push @{$err}, $err_l if defined $err_l;
- push @{$out}, $out_l if defined $out_l;
- last unless ($err_l || $out_l);
- }
- close $err_h;
- close $out_h;
-
- return ($err, $out);
- }
-
- sub update_icon {
- my $text_type = shift;
-
- $statusicon->set_from_stock("gpgApplet-${text_type}");
- }
-
- sub detect_received {
- my $clipboard = shift;
-
- update_icon(detect_text_type(get_validated_clipboard_text()));
- }
-
- sub handle_clipboard_owner_change {
- my $clipboard = shift;
-
- set_freshest_clipboard($clipboard);
- detect_received($clipboard);
- }
-
- sub make_icon_source {
- my $icon = shift;
- my $base = shift;
- my $ext = shift;
- my $size = shift;
-
- my $filename = "/usr/share/pixmaps/gpgApplet/$base/$icon.$ext";
- my $source = Gtk2::IconSource->new();
- $source->set_filename($filename);
- $source->set_direction_wildcarded(1);
- $source->set_state_wildcarded(1);
- if (defined $size) {
- $source->set_size_wildcarded(0);
- $source->set_size($size);
- } else {
- $source->set_size_wildcarded(1);
- }
-
- return $source;
- }
-
- sub init_icons_stock {
- my $factory = shift;
-
- $factory->add_default;
- my @stock_ids = map { "gpgApplet-$_" } qw{ message none signed text };
-
- foreach my $stock_id (@stock_ids) {
- my $iconset = Gtk2::IconSet->new();
- $iconset->add_source(make_icon_source($stock_id, "22x22", "png", 'button'));
- $iconset->add_source(make_icon_source($stock_id, "22x22", "png", 'menu'));
- $iconset->add_source(make_icon_source($stock_id, "22x22", "png", 'large-toolbar'));
- $iconset->add_source(make_icon_source($stock_id, "22x22", "png", 'small-toolbar'));
- $iconset->add_source(make_icon_source($stock_id, "48x48", "png", 'dialog'));
- $iconset->add_source(make_icon_source($stock_id, "scalable", "svg"));
- $factory->add($stock_id, $iconset);
- }
- }
-